﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Threading;
using System.ComponentModel;

namespace SimpleSocket
{

    public delegate void MessageReceivedEventHandler(object sender, ReceivedMessageEventArgs Args);
    public delegate void ConnectionStateEventHandler(object sender, EventArgs Args);

    public class SimpleSocketClient : IDisposable
    {
        internal const int PingCheckTimeOut = 10000;
        internal const int PongResponceTimeOut = 5000;

        public event MessageReceivedEventHandler MessageReceived;
        public event ConnectionStateEventHandler Connected;
        public event ConnectionStateEventHandler Disconnected;

        private bool _isConnected = false;
        public bool IsConnected
        {
            get
            {
                bool currentState = _isConnected;

                if (currentState)
                {
                    if (!ClientSocket.Connected)
                    {
                        currentState = false;
                    }
                    else
                    {
                        try
                        {
                            currentState = !(ClientSocket.Poll(1, SelectMode.SelectRead) && ClientSocket.Available == 0);
                        }
                        catch (SocketException) { currentState = false; }
                    }
                }

                if (currentState != _isConnected)
                    IsConnected = currentState;

                return currentState;
            }

            private set
            {
                if (_isConnected == value)
                    return;

                _isConnected = value;

                if (value)
                {
                    if (Connected != null)
                    {
                        ThreadPool.QueueUserWorkItem((o) =>
                        {
                            Connected.Invoke(this, EventArgs.Empty);
                        });
                    }

                    StartReceiving();
                }
                else
                {
                    MessageReceiver.Reset();
                    if (Disconnected != null)
                    {
                        ThreadPool.QueueUserWorkItem((o) =>
                        {
                            Disconnected.Invoke(this, EventArgs.Empty);
                        });
                    }
                }
            }
        }

        public EndPoint RemoteEndPoint
        {
            get
            {
                return ClientSocket.RemoteEndPoint;
            }
        }

        internal const int ResponceMessageLifeSpan = 10;
        internal Socket ClientSocket { get; private set; }

        private ConcurrentDictionary<uint, Message> ResponceMessages = new ConcurrentDictionary<uint, Message>();
        private Timer CheckTimer;
        private MessageReceiver MessageReceiver;
        private DateTime LastMessageReceived = new DateTime();

        private UInt32 _lastResponceID = 2;
        private UInt32 FreeResponceID
        {
            get
            {
                // cycle through all of the responces if we ever get to the last one reset it.
                if (_lastResponceID == uint.MaxValue)
                    _lastResponceID = 2;
                _lastResponceID++;
                return _lastResponceID;
            }
        }

        private void StartReceiving()
        {
            // Start to receive the message header.
            ClientSocket.BeginReceive(MessageReceiver.ReceiveBuffer, 0, MessageReceiver.ReceiveBuffer.Length, SocketFlags.None, OnReceive, ClientSocket);
        }

        private void OnReceive(IAsyncResult ar)
        {
            try
            {
                int bytesRead = ClientSocket.EndReceive(ar);
                if (bytesRead > 0)
                {
                    LastMessageReceived = DateTime.Now;
                }
                MessageReceiver.DataReceived(bytesRead);
            }
            catch (SocketException) { }

            if (IsConnected)
                StartReceiving();
        }

        public void Connect(string host, int port)
        {
            if (IsConnected)
            {
                throw new Exception("The socket is already connected.");
            }

            ClientSocket.Dispose();
            ClientSocket = null;

            if(ClientSocket == null)
                ClientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            try
            {
                ClientSocket.Connect(host, port);
            }
            catch (SocketException) { }

            if (ClientSocket.Connected)
            {
                IsConnected = true;
            }

        }

        public void Disconnect()
        {
            ClientSocket.Disconnect(false);
            IsConnected = false;
        }

        public SimpleSocketClient()
        {
            ClientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            MessageReceiver = new MessageReceiver(this);

            CheckTimer = new Timer(CheckTimer_Elapsed, null, 1000, Timeout.Infinite);
        }

        public SimpleSocketClient(Socket clientSocket)
            : this()
        {
            ClientSocket = clientSocket;

            if (clientSocket.Connected)
            {
                IsConnected = true;
            }
        }

        void CheckTimer_Elapsed(object state)
        {
            // Disconnect Checking
            if (IsConnected)
            {
                int lastResponceReceived = (int)DateTime.Now.Subtract(LastMessageReceived).TotalSeconds;

                if (lastResponceReceived >= PongResponceTimeOut)
                {
                    // We never received a Pong Responce. The client is probably dead. Lets disconnect.
                    if (IsConnected)
                    {
                        ClientSocket.BeginDisconnect(true, OnDisconnect, null);
                        IsConnected = false;
                    }
                } else if (lastResponceReceived >= PingCheckTimeOut)
                {
                    // we have not received a message in a while. Send a ping request to make sure the connection is still alive.
                    SendPingRequest();
                }
            }

            // Cleanup what is in the ResponceMessages
            for(int x = ResponceMessages.Keys.Count - 1; x >= 0; x--)
            {
                uint responceID = ResponceMessages.Keys.ElementAt(x);
                double secsSinceReceived = DateTime.Now.Subtract(ResponceMessages[responceID].ReceiveComplete).TotalSeconds;
                if (secsSinceReceived > ResponceMessageLifeSpan)
                {
                    Message msg = null;
                    ResponceMessages.TryRemove(responceID, out msg);
                }
            }

            CheckTimer.Change(1000, Timeout.Infinite);
        }

        private void OnDisconnect(IAsyncResult result)
        {
            ClientSocket.EndDisconnect(result);
            IsConnected = false;
        }

        internal void SendResponce(uint responceID, object obj)
        {
            MessageBuilder builder = new MessageBuilder();
            builder.Message = SerilizeObj(obj);
            builder.ResponceID = responceID;
            byte[] rts = builder.GetMessage();
            BaseSendData(builder);
        }

        internal void SendResponce(uint responceID, byte[] data)
        {
            MessageBuilder builder = new MessageBuilder();
            builder.Message = data;
            builder.ResponceID = responceID;
            byte[] rts = builder.GetMessage();
            BaseSendData(builder);
        }

        private void SendCallBack(IAsyncResult ar)
        {
            ClientSocket.EndSend(ar);
        }


        private byte[] SerilizeObj(object obj)
        {
            if (obj == null)
                return null;

            BinaryFormatter bf = new BinaryFormatter();
            MemoryStream stream = new MemoryStream();
            bf.Serialize(stream, obj);
            byte[] returnValue = stream.ToArray();
            stream.Dispose();
            return returnValue;
        }

        private object DeserializeObj(byte[] data)
        {
            BinaryFormatter bf = new BinaryFormatter();
            MemoryStream ms = new MemoryStream(data);
            object value = bf.Deserialize(ms);
            ms.Dispose();
            return value;
        }

        #region Request
        public T Request<T>(int timeOut, int command)
        {
            MessageBuilder builder = new MessageBuilder();
            builder.Command = command;
            builder.RequestResponce = true;
            builder.ResponceID = FreeResponceID;
            return BaseRequestData<T>(builder, timeOut);
        }
        public T Request<T>(int timeOut, string commandText)
        {
            MessageBuilder builder = new MessageBuilder();
            builder.CommandText = commandText;
            builder.RequestResponce = true;
            builder.ResponceID = FreeResponceID;
            return BaseRequestData<T>(builder, timeOut);
        }
        public T Request<T>(int timeOut, int command, byte[] data)
        {
            MessageBuilder builder = new MessageBuilder();
            builder.Command = command;
            builder.RequestResponce = true;
            builder.Message = data;
            builder.ResponceID = FreeResponceID;
            return BaseRequestData<T>(builder, timeOut);
        }
        public T Request<T>(int timeOut, int command, object obj)
        {
            MessageBuilder builder = new MessageBuilder();
            builder.Command = command;
            builder.RequestResponce = true;
            builder.Message = SerilizeObj(obj);
            builder.ResponceID = FreeResponceID;
            return BaseRequestData<T>(builder, timeOut);
        }
        public T Request<T>(int timeOut, string commandText, byte[] data)
        {
            MessageBuilder builder = new MessageBuilder();
            builder.CommandText = commandText;
            builder.RequestResponce = true;
            builder.Message = data;
            builder.ResponceID = FreeResponceID;
            return BaseRequestData<T>(builder, timeOut);
        }
        public T Request<T>(int timeOut, string commandText, object obj)
        {
            MessageBuilder builder = new MessageBuilder();
            builder.CommandText = commandText;
            builder.RequestResponce = true;
            builder.Message = SerilizeObj(obj);
            builder.ResponceID = FreeResponceID;
            return BaseRequestData<T>(builder, timeOut);
        }
        public T Request<T>(int timeOut, int command, string commandText)
        {
            MessageBuilder builder = new MessageBuilder();
            builder.Command = command;
            builder.CommandText = commandText;
            builder.RequestResponce = true;
            builder.ResponceID = FreeResponceID;
            return BaseRequestData<T>(builder, timeOut);
        }
        public T Request<T>(int timeOut, int command, string commandText, byte[] data)
        {
            MessageBuilder builder = new MessageBuilder();
            builder.Command = command;
            builder.CommandText = commandText;
            builder.RequestResponce = true;
            builder.Message = data;
            builder.ResponceID = FreeResponceID;
            return BaseRequestData<T>(builder, timeOut);
        }
        public T Request<T>(int timeOut, int command, string commandText, object obj)
        {
            MessageBuilder builder = new MessageBuilder();
            builder.Command = command;
            builder.CommandText = commandText;
            builder.RequestResponce = true;
            builder.Message = SerilizeObj(obj);
            builder.ResponceID = FreeResponceID;
            return BaseRequestData<T>(builder, timeOut);
        }


        public byte[] Request(int timeOut, int command)
        {
            MessageBuilder builder = new MessageBuilder();
            builder.Command = command;
            builder.RequestResponce = true;
            builder.ResponceID = FreeResponceID;
            return BaseRequestData(builder, timeOut);
        }

        public byte[] Request(int timeOut, string commandText)
        {
            MessageBuilder builder = new MessageBuilder();
            builder.CommandText = commandText;
            builder.RequestResponce = true;
            builder.ResponceID = FreeResponceID;
            return BaseRequestData(builder, timeOut);
        }


        public byte[] Request(int timeOut, int command, byte[] data)
        {
            MessageBuilder builder = new MessageBuilder();
            builder.Command = command;
            builder.RequestResponce = true;
            builder.Message = data;
            builder.ResponceID = FreeResponceID;
            return BaseRequestData(builder, timeOut);
        }
        public byte[] Request(int timeOut, int command, object obj)
        {
            MessageBuilder builder = new MessageBuilder();
            builder.Command = command;
            builder.RequestResponce = true;
            builder.Message = SerilizeObj(obj);
            builder.ResponceID = FreeResponceID;
            return BaseRequestData(builder, timeOut);
        }
        public byte[] Request(int timeOut, string commandText, byte[] data)
        {
            MessageBuilder builder = new MessageBuilder();
            builder.CommandText = commandText;
            builder.RequestResponce = true;
            builder.Message = data;
            builder.ResponceID = FreeResponceID;
            return BaseRequestData(builder, timeOut);
        }
        public byte[] Request(int timeOut, string commandText, object obj)
        {
            MessageBuilder builder = new MessageBuilder();
            builder.CommandText = commandText;
            builder.RequestResponce = true;
            builder.Message = SerilizeObj(obj);
            builder.ResponceID = FreeResponceID;
            return BaseRequestData(builder, timeOut);
        }
        public byte[] Request(int timeOut, int command, string commandText)
        {
            MessageBuilder builder = new MessageBuilder();
            builder.Command = command;
            builder.CommandText = commandText;
            builder.RequestResponce = true;
            builder.ResponceID = FreeResponceID;
            return BaseRequestData(builder, timeOut);
        }
        public byte[] Request(int timeOut, int command, string commandText, byte[] data)
        {
            MessageBuilder builder = new MessageBuilder();
            builder.Command = command;
            builder.CommandText = commandText;
            builder.RequestResponce = true;
            builder.Message = data;
            builder.ResponceID = FreeResponceID;
            return BaseRequestData(builder, timeOut);
        }
        public byte[] Request(int timeOut, int command, string commandText, object obj)
        {
            MessageBuilder builder = new MessageBuilder();
            builder.Command = command;
            builder.CommandText = commandText;
            builder.RequestResponce = true;
            builder.Message = SerilizeObj(obj);
            builder.ResponceID = FreeResponceID;
            return BaseRequestData(builder, timeOut);
        }
        #endregion

        #region Send

        public void Send(int command)
        {
            MessageBuilder builder = new MessageBuilder();
            builder.Command = command;
            BaseSendData(builder);
        }
        public void Send(string commandText)
        {
            MessageBuilder builder = new MessageBuilder();
            builder.CommandText = commandText;
            BaseSendData(builder);
        }
        public void Send(int command, string commandText)
        {
            MessageBuilder builder = new MessageBuilder();
            builder.Command = command;
            builder.CommandText = commandText;
            BaseSendData(builder);
        }
        public void Send(int command, byte[] data)
        {
            MessageBuilder builder = new MessageBuilder();
            builder.Command = command;
            builder.Message = data;
            BaseSendData(builder);
        }
        public void Send(string commandText, byte[] data)
        {
            MessageBuilder builder = new MessageBuilder();
            builder.CommandText = commandText;
            builder.Message = data;
            BaseSendData(builder);
        }
        public void Send(int command, string commandText, byte[] data)
        {
            MessageBuilder builder = new MessageBuilder();
            builder.Command = command;
            builder.CommandText = commandText;
            builder.Message = data;
            BaseSendData(builder);
        }
        public void Send(int command, object obj)
        {
            MessageBuilder builder = new MessageBuilder();
            builder.Command = command;
            builder.Message = SerilizeObj(obj);
            BaseSendData(builder);
        }
        public void Send(string commandText, object obj)
        {
            MessageBuilder builder = new MessageBuilder();
            builder.CommandText = commandText;
            builder.Message = SerilizeObj(obj);
            BaseSendData(builder);
        }
        public void Send(int command, string commandText, object obj)
        {
            MessageBuilder builder = new MessageBuilder();
            builder.Command = command;
            builder.CommandText = commandText;
            builder.Message = SerilizeObj(obj);
            BaseSendData(builder);
        }

        #endregion

        private void BaseSendData(MessageBuilder builder)
        {
            if (!IsConnected)
                throw new Exception("Send Failed, Not connected to server.");

            byte[] rts = builder.GetMessage();
            ClientSocket.BeginSend(rts, 0, rts.Length, SocketFlags.None, SendCallBack, this);
        }

        private T BaseRequestData<T>(MessageBuilder builder, int timeOut)
        {
            byte[] data = BaseRequestData(builder, timeOut);
            
            if(data == null)
                return default(T);

            return (T)DeserializeObj(data);
        }

        private byte[] BaseRequestData(MessageBuilder builder, int timeOut)
        {

            if (!IsConnected)
                throw new Exception("Request Failed, Not connected to server.");

            byte[] rts = builder.GetMessage();
            var result = ClientSocket.BeginSend(rts, 0, rts.Length, SocketFlags.None, SendCallBack, this);

            var sw = System.Diagnostics.Stopwatch.StartNew();
            result.AsyncWaitHandle.WaitOne(timeOut * 1000);

            while (sw.Elapsed.TotalSeconds < timeOut)
            {
                System.Threading.Thread.Sleep(0);
                if (ResponceMessages.ContainsKey(builder.ResponceID) || !ClientSocket.Connected)
                    break;
            }

            sw.Stop();

            if (ResponceMessages.ContainsKey(builder.ResponceID))
            {
                Message msg = null;
                ResponceMessages.TryRemove(builder.ResponceID, out msg);
                return msg.ReceivedData;
            }

            return null;
        }

        internal void SendPingRequest()
        {
            byte[] pingRequest = BitConverter.GetBytes((int)MessageFlags.Ping);
            ClientSocket.BeginSend(pingRequest, 0, pingRequest.Length, SocketFlags.None, SendCallBack, this);
        }

        internal void SendPongResponce()
        {
            byte[] pingRequest = BitConverter.GetBytes((int)MessageFlags.Pong);
            ClientSocket.BeginSend(pingRequest, 0, pingRequest.Length, SocketFlags.None, SendCallBack, this);
        }

        internal void MessageComplete(MessageFlags flags, Message message)
        {
            if(Helpers.HasFlag(flags, MessageFlags.Ping))
            {
                SendPongResponce();
            }
            else if (Helpers.HasFlag(flags, MessageFlags.Pong))
            {
                // nothing to do here.
                // the pong responce is just used to update the last received date.
            }
            else if (message.ResponceID > 0 && message.ResponceRequested == false)
            {
                ResponceMessages.GetOrAdd(message.ResponceID, message);
            }
            else if (MessageReceived != null)
            {
                ReceivedMessageEventArgs args = new ReceivedMessageEventArgs(this, message);

                ThreadPool.QueueUserWorkItem((o) =>
                {
                    MessageReceived.Invoke(this, args);
                });
            }
        }

        public void Dispose()
        {
            MessageReceiver.Dispose();
            ClientSocket.Dispose();
            CheckTimer.Dispose();
            MessageReceived = null;
            MessageReceiver = null;
            Connected = null;
            Disconnected = null;
        }
    }
}
